/*
 * Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
/*
 * Copyright 2003-2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.sun.org.apache.xerces.internal.xinclude;

import java.io.CharConversionException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.xml.XMLConstants;

import com.sun.org.apache.xerces.internal.impl.Constants;
import com.sun.org.apache.xerces.internal.impl.XMLEntityManager;
import com.sun.org.apache.xerces.internal.impl.XMLErrorReporter;
import com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException;
import com.sun.org.apache.xerces.internal.impl.msg.XMLMessageFormatter;
import com.sun.org.apache.xerces.internal.util.AugmentationsImpl;
import com.sun.org.apache.xerces.internal.util.HTTPInputSource;
import com.sun.org.apache.xerces.internal.util.IntStack;
import com.sun.org.apache.xerces.internal.util.ParserConfigurationSettings;
import com.sun.org.apache.xerces.internal.util.SymbolTable;
import com.sun.org.apache.xerces.internal.util.URI;
import com.sun.org.apache.xerces.internal.util.XMLAttributesImpl;
import com.sun.org.apache.xerces.internal.util.XMLResourceIdentifierImpl;
import com.sun.org.apache.xerces.internal.util.XMLChar;
import com.sun.org.apache.xerces.internal.util.XMLSymbols;
import com.sun.org.apache.xerces.internal.util.URI.MalformedURIException;
import com.sun.org.apache.xerces.internal.utils.XMLSecurityManager;
import com.sun.org.apache.xerces.internal.xni.Augmentations;
import com.sun.org.apache.xerces.internal.xni.NamespaceContext;
import com.sun.org.apache.xerces.internal.xni.QName;
import com.sun.org.apache.xerces.internal.xni.XMLAttributes;
import com.sun.org.apache.xerces.internal.xni.XMLDTDHandler;
import com.sun.org.apache.xerces.internal.xni.XMLDocumentHandler;
import com.sun.org.apache.xerces.internal.xni.XMLLocator;
import com.sun.org.apache.xerces.internal.xni.XMLResourceIdentifier;
import com.sun.org.apache.xerces.internal.xni.XMLString;
import com.sun.org.apache.xerces.internal.xni.XNIException;
import com.sun.org.apache.xerces.internal.xni.parser.XMLComponent;
import com.sun.org.apache.xerces.internal.xni.parser.XMLComponentManager;
import com.sun.org.apache.xerces.internal.xni.parser.XMLConfigurationException;
import com.sun.org.apache.xerces.internal.xni.parser.XMLDTDFilter;
import com.sun.org.apache.xerces.internal.xni.parser.XMLDTDSource;
import com.sun.org.apache.xerces.internal.xni.parser.XMLDocumentFilter;
import com.sun.org.apache.xerces.internal.xni.parser.XMLDocumentSource;
import com.sun.org.apache.xerces.internal.xni.parser.XMLEntityResolver;
import com.sun.org.apache.xerces.internal.xni.parser.XMLInputSource;
import com.sun.org.apache.xerces.internal.xni.parser.XMLParserConfiguration;
import com.sun.org.apache.xerces.internal.xpointer.XPointerHandler;
import com.sun.org.apache.xerces.internal.xpointer.XPointerProcessor;
import com.sun.org.apache.xerces.internal.utils.ObjectFactory;
import com.sun.org.apache.xerces.internal.utils.XMLSecurityPropertyManager;
import java.util.Objects;

/**
 * <p>
 * This is a pipeline component which performs XInclude handling, according to the
 * W3C specification for XML Inclusions.
 * </p>
 * <p>
 * This component analyzes each event in the pipeline, looking for &lt;include&gt;
 * elements. An &lt;include&gt; element is one which has a namespace of
 * <code>http://www.w3.org/2001/XInclude</code> and a localname of <code>include</code>.
 * When it finds an &lt;include&gt; element, it attempts to include the file specified
 * in the <code>href</code> attribute of the element.  If inclusion succeeds, all
 * children of the &lt;include&gt; element are ignored (with the exception of
 * checking for invalid children as outlined in the specification).  If the inclusion
 * fails, the &lt;fallback&gt; child of the &lt;include&gt; element is processed.
 * </p>
 * <p>
 * See the <a href="http://www.w3.org/TR/xinclude/">XInclude specification</a> for
 * more information on how XInclude is to be used.
 * </p>
 * <p>
 * This component requires the following features and properties from the
 * component manager that uses it:
 * <ul>
 * <li>http://xml.org/sax/features/allow-dtd-events-after-endDTD</li>
 * <li>http://apache.org/xml/properties/internal/error-reporter</li>
 * <li>http://apache.org/xml/properties/internal/entity-resolver</li>
 * </ul>
 * Optional property:
 * <ul>
 * <li>http://apache.org/xml/properties/input-buffer-size</li>
 * </ul>
 *
 * Furthermore, the <code>NamespaceContext</code> used in the pipeline is required
 * to be an instance of <code>XIncludeNamespaceSupport</code>.
 * </p>
 * <p>
 * Currently, this implementation has only partial support for the XInclude specification.
 * Specifically, it is missing support for XPointer document fragments.  Thus, only whole
 * documents can be included using this component in the pipeline.
 * </p>
 *
 * @author Peter McCracken, IBM
 * @author Michael Glavassevich, IBM
 * @version $Id: XIncludeHandler.java,v 1.7 2010-11-01 04:40:18 joehw Exp $
 * @see XIncludeNamespaceSupport
 */
public class XIncludeHandler
    implements XMLComponent, XMLDocumentFilter, XMLDTDFilter {

  public final static String XINCLUDE_DEFAULT_CONFIGURATION =
      "com.sun.org.apache.xerces.internal.parsers.XIncludeParserConfiguration";
  public final static String HTTP_ACCEPT = "Accept";
  public final static String HTTP_ACCEPT_LANGUAGE = "Accept-Language";
  public final static String XPOINTER = "xpointer";

  public final static String XINCLUDE_NS_URI =
      "http://www.w3.org/2001/XInclude".intern();
  public final static String XINCLUDE_INCLUDE = "include".intern();
  public final static String XINCLUDE_FALLBACK = "fallback".intern();

  public final static String XINCLUDE_PARSE_XML = "xml".intern();
  public final static String XINCLUDE_PARSE_TEXT = "text".intern();

  public final static String XINCLUDE_ATTR_HREF = "href".intern();
  public final static String XINCLUDE_ATTR_PARSE = "parse".intern();
  public final static String XINCLUDE_ATTR_ENCODING = "encoding".intern();
  public final static String XINCLUDE_ATTR_ACCEPT = "accept".intern();
  public final static String XINCLUDE_ATTR_ACCEPT_LANGUAGE = "accept-language".intern();

  // Top Level Information Items have [included] property in infoset
  public final static String XINCLUDE_INCLUDED = "[included]".intern();

  /**
   * The identifier for the Augmentation that contains the current base URI
   */
  public final static String CURRENT_BASE_URI = "currentBaseURI";

  // used for adding [base URI] attributes
  public final static String XINCLUDE_BASE = "base".intern();
  public final static QName XML_BASE_QNAME =
      new QName(
          XMLSymbols.PREFIX_XML,
          XINCLUDE_BASE,
          (XMLSymbols.PREFIX_XML + ":" + XINCLUDE_BASE).intern(),
          NamespaceContext.XML_URI);

  // used for adding [language] attributes
  public final static String XINCLUDE_LANG = "lang".intern();
  public final static QName XML_LANG_QNAME =
      new QName(
          XMLSymbols.PREFIX_XML,
          XINCLUDE_LANG,
          (XMLSymbols.PREFIX_XML + ":" + XINCLUDE_LANG).intern(),
          NamespaceContext.XML_URI);

  public final static QName NEW_NS_ATTR_QNAME =
      new QName(
          XMLSymbols.PREFIX_XMLNS,
          "",
          XMLSymbols.PREFIX_XMLNS + ":",
          NamespaceContext.XMLNS_URI);

  // Processing States
  private final static int STATE_NORMAL_PROCESSING = 1;
  // we go into this state after a successful include (thus we ignore the children
  // of the include) or after a fallback
  private final static int STATE_IGNORE = 2;
  // we go into this state after a failed include.  If we don't encounter a fallback
  // before we reach the end include tag, it's a fatal error
  private final static int STATE_EXPECT_FALLBACK = 3;

  // recognized features and properties

  /**
   * Feature identifier: validation.
   */
  protected static final String VALIDATION =
      Constants.SAX_FEATURE_PREFIX + Constants.VALIDATION_FEATURE;

  /**
   * Feature identifier: schema validation.
   */
  protected static final String SCHEMA_VALIDATION =
      Constants.XERCES_FEATURE_PREFIX + Constants.SCHEMA_VALIDATION_FEATURE;

  /**
   * Feature identifier: dynamic validation.
   */
  protected static final String DYNAMIC_VALIDATION =
      Constants.XERCES_FEATURE_PREFIX + Constants.DYNAMIC_VALIDATION_FEATURE;

  /**
   * Feature identifier: allow notation and unparsed entity events to be sent out of order.
   */
  protected static final String ALLOW_UE_AND_NOTATION_EVENTS =
      Constants.SAX_FEATURE_PREFIX
          + Constants.ALLOW_DTD_EVENTS_AFTER_ENDDTD_FEATURE;

  /**
   * Feature identifier: fixup base URIs.
   */
  protected static final String XINCLUDE_FIXUP_BASE_URIS =
      Constants.XERCES_FEATURE_PREFIX + Constants.XINCLUDE_FIXUP_BASE_URIS_FEATURE;

  /**
   * Feature identifier: fixup language.
   */
  protected static final String XINCLUDE_FIXUP_LANGUAGE =
      Constants.XERCES_FEATURE_PREFIX + Constants.XINCLUDE_FIXUP_LANGUAGE_FEATURE;

  /**
   * Property identifier: symbol table.
   */
  protected static final String SYMBOL_TABLE =
      Constants.XERCES_PROPERTY_PREFIX + Constants.SYMBOL_TABLE_PROPERTY;

  /**
   * Property identifier: error reporter.
   */
  protected static final String ERROR_REPORTER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.ERROR_REPORTER_PROPERTY;

  /**
   * Property identifier: entity resolver.
   */
  protected static final String ENTITY_RESOLVER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.ENTITY_RESOLVER_PROPERTY;

  /**
   * property identifier: security manager.
   */
  protected static final String SECURITY_MANAGER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.SECURITY_MANAGER_PROPERTY;

  /**
   * property identifier: buffer size.
   */
  public static final String BUFFER_SIZE =
      Constants.XERCES_PROPERTY_PREFIX + Constants.BUFFER_SIZE_PROPERTY;

  protected static final String PARSER_SETTINGS =
      Constants.XERCES_FEATURE_PREFIX + Constants.PARSER_SETTINGS;

  /**
   * property identifier: XML security property manager.
   */
  protected static final String XML_SECURITY_PROPERTY_MANAGER =
      Constants.XML_SECURITY_PROPERTY_MANAGER;

  /**
   * Recognized features.
   */
  private static final String[] RECOGNIZED_FEATURES =
      {ALLOW_UE_AND_NOTATION_EVENTS, XINCLUDE_FIXUP_BASE_URIS, XINCLUDE_FIXUP_LANGUAGE};

  /**
   * Feature defaults.
   */
  private static final Boolean[] FEATURE_DEFAULTS = {Boolean.TRUE, Boolean.TRUE, Boolean.TRUE};

  /**
   * Recognized properties.
   */
  private static final String[] RECOGNIZED_PROPERTIES =
      {ERROR_REPORTER, ENTITY_RESOLVER, SECURITY_MANAGER, BUFFER_SIZE};

  /**
   * Property defaults.
   */
  private static final Object[] PROPERTY_DEFAULTS = {null, null, null,
      new Integer(XMLEntityManager.DEFAULT_BUFFER_SIZE)};

  // instance variables

  // for XMLDocumentFilter
  protected XMLDocumentHandler fDocumentHandler;
  protected XMLDocumentSource fDocumentSource;

  // for XMLDTDFilter
  protected XMLDTDHandler fDTDHandler;
  protected XMLDTDSource fDTDSource;

  // for XIncludeHandler
  protected XIncludeHandler fParentXIncludeHandler;

  // for buffer size in XIncludeTextReader
  protected int fBufferSize = XMLEntityManager.DEFAULT_BUFFER_SIZE;

  // It "feels wrong" to store this value here.  However,
  // calculating it can be time consuming, so we cache it.
  // It's never going to change in the lifetime of this XIncludeHandler
  protected String fParentRelativeURI;

  // we cache the child parser configuration, so we don't have to re-create
  // the objects when the parser is re-used
  protected XMLParserConfiguration fChildConfig;

  // The cached child parser configuration, may contain a
  // XInclude or XPointer Handler.  Cache both these
  protected XMLParserConfiguration fXIncludeChildConfig;
  protected XMLParserConfiguration fXPointerChildConfig;

  // The XPointerProcessor
  protected XPointerProcessor fXPtrProcessor = null;

  protected XMLLocator fDocLocation;
  protected XIncludeMessageFormatter fXIncludeMessageFormatter = new XIncludeMessageFormatter();
  protected XIncludeNamespaceSupport fNamespaceContext;
  protected SymbolTable fSymbolTable;
  protected XMLErrorReporter fErrorReporter;
  protected XMLEntityResolver fEntityResolver;
  protected XMLSecurityManager fSecurityManager;
  protected XMLSecurityPropertyManager fSecurityPropertyMgr;

  // these are needed for text include processing
  protected XIncludeTextReader fXInclude10TextReader;
  protected XIncludeTextReader fXInclude11TextReader;

  // these are needed for XML Base processing
  protected XMLResourceIdentifier fCurrentBaseURI;
  protected IntStack fBaseURIScope;
  protected Stack fBaseURI;
  protected Stack fLiteralSystemID;
  protected Stack fExpandedSystemID;

  // these are needed for Language Fixup
  protected IntStack fLanguageScope;
  protected Stack fLanguageStack;
  protected String fCurrentLanguage;

  // used for passing features on to child XIncludeHandler objects
  protected ParserConfigurationSettings fSettings;

  // The current element depth.  We start at depth 0 (before we've reached any elements).
  // The first element is at depth 1.
  private int fDepth;

  // The current element depth of the result infoset.
  private int fResultDepth;

  // this value must be at least 1
  private static final int INITIAL_SIZE = 8;

  // Used to ensure that fallbacks are always children of include elements,
  // and that include elements are never children of other include elements.
  // An index contains true if the ancestor of the current element which resides
  // at that depth was an include element.
  private boolean[] fSawInclude = new boolean[INITIAL_SIZE];

  // Ensures that only one fallback element can be at a single depth.
  // An index contains true if we have seen any fallback elements at that depth,
  // and it is only reset to false when the end tag of the parent is encountered.
  private boolean[] fSawFallback = new boolean[INITIAL_SIZE];

  // The state of the processor at each given depth.
  private int[] fState = new int[INITIAL_SIZE];

  // buffering the necessary DTD events
  private ArrayList fNotations;
  private ArrayList fUnparsedEntities;

  // flags which control whether base URI or language fixup is performed.
  private boolean fFixupBaseURIs = true;
  private boolean fFixupLanguage = true;

  // for SAX compatibility.
  // Has the value of the ALLOW_UE_AND_NOTATION_EVENTS feature
  private boolean fSendUEAndNotationEvents;

  // track the version of the document being parsed
  private boolean fIsXML11;

  // track whether a DTD is being parsed
  private boolean fInDTD;

  // track whether the root element of the result infoset has been processed
  private boolean fSeenRootElement;

  // track whether the child config needs its features refreshed
  private boolean fNeedCopyFeatures = true;

  // Constructors

  public XIncludeHandler() {
    fDepth = 0;

    fSawFallback[fDepth] = false;
    fSawInclude[fDepth] = false;
    fState[fDepth] = STATE_NORMAL_PROCESSING;
    fNotations = new ArrayList();
    fUnparsedEntities = new ArrayList();

    fBaseURIScope = new IntStack();
    fBaseURI = new Stack();
    fLiteralSystemID = new Stack();
    fExpandedSystemID = new Stack();
    fCurrentBaseURI = new XMLResourceIdentifierImpl();

    fLanguageScope = new IntStack();
    fLanguageStack = new Stack();
    fCurrentLanguage = null;
  }

  // XMLComponent methods

  @Override
  public void reset(XMLComponentManager componentManager)
      throws XNIException {
    fNamespaceContext = null;
    fDepth = 0;
    fResultDepth = isRootDocument() ? 0 : fParentXIncludeHandler.getResultDepth();
    fNotations.clear();
    fUnparsedEntities.clear();
    fParentRelativeURI = null;
    fIsXML11 = false;
    fInDTD = false;
    fSeenRootElement = false;

    fBaseURIScope.clear();
    fBaseURI.clear();
    fLiteralSystemID.clear();
    fExpandedSystemID.clear();
    fLanguageScope.clear();
    fLanguageStack.clear();

    // REVISIT: Find a better method for maintaining
    // the state of the XInclude processor. These arrays
    // can potentially grow quite large. Cleaning them
    // out on reset may be very time consuming. -- mrglavas
    //
    // clear the previous settings from the arrays
    for (int i = 0; i < fState.length; ++i) {
      fState[i] = STATE_NORMAL_PROCESSING;
    }
    for (int i = 0; i < fSawFallback.length; ++i) {
      fSawFallback[i] = false;
    }
    for (int i = 0; i < fSawInclude.length; ++i) {
      fSawInclude[i] = false;
    }

    try {
      if (!componentManager.getFeature(PARSER_SETTINGS)) {
        // if parser settings have not changed return.
        return;
      }
    } catch (XMLConfigurationException e) {
    }

    // parser settings changed. Need to refresh features on child config.
    fNeedCopyFeatures = true;

    try {
      fSendUEAndNotationEvents =
          componentManager.getFeature(ALLOW_UE_AND_NOTATION_EVENTS);
      if (fChildConfig != null) {
        fChildConfig.setFeature(
            ALLOW_UE_AND_NOTATION_EVENTS,
            fSendUEAndNotationEvents);
      }
    } catch (XMLConfigurationException e) {
    }

    try {
      fFixupBaseURIs =
          componentManager.getFeature(XINCLUDE_FIXUP_BASE_URIS);
      if (fChildConfig != null) {
        fChildConfig.setFeature(
            XINCLUDE_FIXUP_BASE_URIS,
            fFixupBaseURIs);
      }
    } catch (XMLConfigurationException e) {
      fFixupBaseURIs = true;
    }

    try {
      fFixupLanguage =
          componentManager.getFeature(XINCLUDE_FIXUP_LANGUAGE);
      if (fChildConfig != null) {
        fChildConfig.setFeature(
            XINCLUDE_FIXUP_LANGUAGE,
            fFixupLanguage);
      }
    } catch (XMLConfigurationException e) {
      fFixupLanguage = true;
    }

    // Get symbol table.
    try {
      SymbolTable value =
          (SymbolTable) componentManager.getProperty(SYMBOL_TABLE);
      if (value != null) {
        fSymbolTable = value;
        if (fChildConfig != null) {
          fChildConfig.setProperty(SYMBOL_TABLE, value);
        }
      }
    } catch (XMLConfigurationException e) {
      fSymbolTable = null;
    }

    // Get error reporter.
    try {
      XMLErrorReporter value =
          (XMLErrorReporter) componentManager.getProperty(ERROR_REPORTER);
      if (value != null) {
        setErrorReporter(value);
        if (fChildConfig != null) {
          fChildConfig.setProperty(ERROR_REPORTER, value);
        }
      }
    } catch (XMLConfigurationException e) {
      fErrorReporter = null;
    }

    // Get entity resolver.
    try {
      XMLEntityResolver value =
          (XMLEntityResolver) componentManager.getProperty(
              ENTITY_RESOLVER);

      if (value != null) {
        fEntityResolver = value;
        if (fChildConfig != null) {
          fChildConfig.setProperty(ENTITY_RESOLVER, value);
        }
      }
    } catch (XMLConfigurationException e) {
      fEntityResolver = null;
    }

    // Get security manager.
    try {
      XMLSecurityManager value =
          (XMLSecurityManager) componentManager.getProperty(
              SECURITY_MANAGER);

      if (value != null) {
        fSecurityManager = value;
        if (fChildConfig != null) {
          fChildConfig.setProperty(SECURITY_MANAGER, value);
        }
      }
    } catch (XMLConfigurationException e) {
      fSecurityManager = null;
    }

    fSecurityPropertyMgr = (XMLSecurityPropertyManager)
        componentManager.getProperty(Constants.XML_SECURITY_PROPERTY_MANAGER);

    // Get buffer size.
    try {
      Integer value =
          (Integer) componentManager.getProperty(
              BUFFER_SIZE);

      if (value != null && value.intValue() > 0) {
        fBufferSize = value.intValue();
        if (fChildConfig != null) {
          fChildConfig.setProperty(BUFFER_SIZE, value);
        }
      } else {
        fBufferSize = ((Integer) getPropertyDefault(BUFFER_SIZE)).intValue();
      }
    } catch (XMLConfigurationException e) {
      fBufferSize = ((Integer) getPropertyDefault(BUFFER_SIZE)).intValue();
    }

    // Reset XML 1.0 text reader.
    if (fXInclude10TextReader != null) {
      fXInclude10TextReader.setBufferSize(fBufferSize);
    }
    // Reset XML 1.1 text reader.
    if (fXInclude11TextReader != null) {
      fXInclude11TextReader.setBufferSize(fBufferSize);
    }

    fSettings = new ParserConfigurationSettings();
    copyFeatures(componentManager, fSettings);

    // We don't want a schema validator on the new pipeline,
    // so if it was enabled, we set the feature to false. If
    // the validation feature was also enabled we turn on
    // dynamic validation, so that DTD validation is performed
    // on the included documents only if they have a DOCTYPE.
    // This is consistent with the behaviour on the main pipeline.
    try {
      if (componentManager.getFeature(SCHEMA_VALIDATION)) {
        fSettings.setFeature(SCHEMA_VALIDATION, false);
        if (componentManager.getFeature(VALIDATION)) {
          fSettings.setFeature(DYNAMIC_VALIDATION, true);
        }
      }
    } catch (XMLConfigurationException e) {
    }

    // Don't reset fChildConfig -- we don't want it to share the same components.
    // It will be reset when it is actually used to parse something.
  } // reset(XMLComponentManager)

  /**
   * Returns a list of feature identifiers that are recognized by
   * this component. This method may return null if no features
   * are recognized by this component.
   */
  @Override
  public String[] getRecognizedFeatures() {
    return (String[]) (RECOGNIZED_FEATURES.clone());
  } // getRecognizedFeatures():String[]

  /**
   * Sets the state of a feature. This method is called by the component
   * manager any time after reset when a feature changes state.
   * <p>
   * <strong>Note:</strong> Components should silently ignore features
   * that do not affect the operation of the component.
   *
   * @param featureId The feature identifier.
   * @param state The state of the feature.
   * @throws SAXNotRecognizedException The component should not throw this exception.
   * @throws SAXNotSupportedException The component should not throw this exception.
   */
  @Override
  public void setFeature(String featureId, boolean state)
      throws XMLConfigurationException {
    if (featureId.equals(ALLOW_UE_AND_NOTATION_EVENTS)) {
      fSendUEAndNotationEvents = state;
    }
    if (fSettings != null) {
      fNeedCopyFeatures = true;
      fSettings.setFeature(featureId, state);
    }
  } // setFeature(String,boolean)

  /**
   * Returns a list of property identifiers that are recognized by
   * this component. This method may return null if no properties
   * are recognized by this component.
   */
  @Override
  public String[] getRecognizedProperties() {
    return (String[]) (RECOGNIZED_PROPERTIES.clone());
  } // getRecognizedProperties():String[]

  /**
   * Sets the value of a property. This method is called by the component
   * manager any time after reset when a property changes value.
   * <p>
   * <strong>Note:</strong> Components should silently ignore properties
   * that do not affect the operation of the component.
   *
   * @param propertyId The property identifier.
   * @param value The value of the property.
   * @throws SAXNotRecognizedException The component should not throw this exception.
   * @throws SAXNotSupportedException The component should not throw this exception.
   */
  @Override
  public void setProperty(String propertyId, Object value)
      throws XMLConfigurationException {
    if (propertyId.equals(SYMBOL_TABLE)) {
      fSymbolTable = (SymbolTable) value;
      if (fChildConfig != null) {
        fChildConfig.setProperty(propertyId, value);
      }
      return;
    }
    if (propertyId.equals(ERROR_REPORTER)) {
      setErrorReporter((XMLErrorReporter) value);
      if (fChildConfig != null) {
        fChildConfig.setProperty(propertyId, value);
      }
      return;
    }
    if (propertyId.equals(ENTITY_RESOLVER)) {
      fEntityResolver = (XMLEntityResolver) value;
      if (fChildConfig != null) {
        fChildConfig.setProperty(propertyId, value);
      }
      return;
    }
    if (propertyId.equals(SECURITY_MANAGER)) {
      fSecurityManager = (XMLSecurityManager) value;
      if (fChildConfig != null) {
        fChildConfig.setProperty(propertyId, value);
      }
      return;
    }
    if (propertyId.equals(XML_SECURITY_PROPERTY_MANAGER)) {
      fSecurityPropertyMgr = (XMLSecurityPropertyManager) value;

      if (fChildConfig != null) {
        fChildConfig.setProperty(XML_SECURITY_PROPERTY_MANAGER, value);
      }

      return;
    }

    if (propertyId.equals(BUFFER_SIZE)) {
      Integer bufferSize = (Integer) value;
      if (fChildConfig != null) {
        fChildConfig.setProperty(propertyId, value);
      }
      if (bufferSize != null && bufferSize.intValue() > 0) {
        fBufferSize = bufferSize.intValue();
        // Reset XML 1.0 text reader.
        if (fXInclude10TextReader != null) {
          fXInclude10TextReader.setBufferSize(fBufferSize);
        }
        // Reset XML 1.1 text reader.
        if (fXInclude11TextReader != null) {
          fXInclude11TextReader.setBufferSize(fBufferSize);
        }
      }
      return;
    }

  } // setProperty(String,Object)

  /**
   * Returns the default state for a feature, or null if this
   * component does not want to report a default value for this
   * feature.
   *
   * @param featureId The feature identifier.
   * @since Xerces 2.2.0
   */
  @Override
  public Boolean getFeatureDefault(String featureId) {
    for (int i = 0; i < RECOGNIZED_FEATURES.length; i++) {
      if (RECOGNIZED_FEATURES[i].equals(featureId)) {
        return FEATURE_DEFAULTS[i];
      }
    }
    return null;
  } // getFeatureDefault(String):Boolean

  /**
   * Returns the default state for a property, or null if this
   * component does not want to report a default value for this
   * property.
   *
   * @param propertyId The property identifier.
   * @since Xerces 2.2.0
   */
  @Override
  public Object getPropertyDefault(String propertyId) {
    for (int i = 0; i < RECOGNIZED_PROPERTIES.length; i++) {
      if (RECOGNIZED_PROPERTIES[i].equals(propertyId)) {
        return PROPERTY_DEFAULTS[i];
      }
    }
    return null;
  } // getPropertyDefault(String):Object

  @Override
  public void setDocumentHandler(XMLDocumentHandler handler) {
    fDocumentHandler = handler;
  }

  @Override
  public XMLDocumentHandler getDocumentHandler() {
    return fDocumentHandler;
  }

  // XMLDocumentHandler methods

  /**
   * Event sent at the start of the document.
   *
   * A fatal error will occur here, if it is detected that this document has been processed
   * before.
   *
   * This event is only passed on to the document handler if this is the root document.
   */
  @Override
  public void startDocument(
      XMLLocator locator,
      String encoding,
      NamespaceContext namespaceContext,
      Augmentations augs)
      throws XNIException {

    // we do this to ensure that the proper location is reported in errors
    // otherwise, the locator from the root document would always be used
    fErrorReporter.setDocumentLocator(locator);

    if (!isRootDocument()
        && fParentXIncludeHandler.searchForRecursiveIncludes(locator)) {
      reportFatalError(
          "RecursiveInclude",
          new Object[]{locator.getExpandedSystemId()});
    }

    if (!(namespaceContext instanceof XIncludeNamespaceSupport)) {
      reportFatalError("IncompatibleNamespaceContext");
    }
    fNamespaceContext = (XIncludeNamespaceSupport) namespaceContext;
    fDocLocation = locator;

    // initialize the current base URI
    fCurrentBaseURI.setBaseSystemId(locator.getBaseSystemId());
    fCurrentBaseURI.setExpandedSystemId(locator.getExpandedSystemId());
    fCurrentBaseURI.setLiteralSystemId(locator.getLiteralSystemId());
    saveBaseURI();
    if (augs == null) {
      augs = new AugmentationsImpl();
    }
    augs.putItem(CURRENT_BASE_URI, fCurrentBaseURI);

    // initialize the current language
    fCurrentLanguage = XMLSymbols.EMPTY_STRING;
    saveLanguage(fCurrentLanguage);

    if (isRootDocument() && fDocumentHandler != null) {
      fDocumentHandler.startDocument(
          locator,
          encoding,
          namespaceContext,
          augs);
    }
  }

  @Override
  public void xmlDecl(
      String version,
      String encoding,
      String standalone,
      Augmentations augs)
      throws XNIException {
    fIsXML11 = "1.1".equals(version);
    if (isRootDocument() && fDocumentHandler != null) {
      fDocumentHandler.xmlDecl(version, encoding, standalone, augs);
    }
  }

  @Override
  public void doctypeDecl(
      String rootElement,
      String publicId,
      String systemId,
      Augmentations augs)
      throws XNIException {
    if (isRootDocument() && fDocumentHandler != null) {
      fDocumentHandler.doctypeDecl(rootElement, publicId, systemId, augs);
    }
  }

  @Override
  public void comment(XMLString text, Augmentations augs)
      throws XNIException {
    if (!fInDTD) {
      if (fDocumentHandler != null
          && getState() == STATE_NORMAL_PROCESSING) {
        fDepth++;
        augs = modifyAugmentations(augs);
        fDocumentHandler.comment(text, augs);
        fDepth--;
      }
    } else if (fDTDHandler != null) {
      fDTDHandler.comment(text, augs);
    }
  }

  @Override
  public void processingInstruction(
      String target,
      XMLString data,
      Augmentations augs)
      throws XNIException {
    if (!fInDTD) {
      if (fDocumentHandler != null
          && getState() == STATE_NORMAL_PROCESSING) {
        // we need to change the depth like this so that modifyAugmentations() works
        fDepth++;
        augs = modifyAugmentations(augs);
        fDocumentHandler.processingInstruction(target, data, augs);
        fDepth--;
      }
    } else if (fDTDHandler != null) {
      fDTDHandler.processingInstruction(target, data, augs);
    }
  }

  @Override
  public void startElement(
      QName element,
      XMLAttributes attributes,
      Augmentations augs)
      throws XNIException {
    fDepth++;
    int lastState = getState(fDepth - 1);
    // If the last two states were fallback then this must be a descendant of an include
    // child which isn't a fallback. The specification says we should ignore such elements
    // and their children.
    if (lastState == STATE_EXPECT_FALLBACK && getState(fDepth - 2) == STATE_EXPECT_FALLBACK) {
      setState(STATE_IGNORE);
    } else {
      setState(lastState);
    }

    // we process the xml:base and xml:lang attributes regardless
    // of what type of element it is.
    processXMLBaseAttributes(attributes);
    if (fFixupLanguage) {
      processXMLLangAttributes(attributes);
    }

    if (isIncludeElement(element)) {
      boolean success = this.handleIncludeElement(attributes);
      if (success) {
        setState(STATE_IGNORE);
      } else {
        setState(STATE_EXPECT_FALLBACK);
      }
    } else if (isFallbackElement(element)) {
      this.handleFallbackElement();
    } else if (hasXIncludeNamespace(element)) {
      if (getSawInclude(fDepth - 1)) {
        reportFatalError(
            "IncludeChild",
            new Object[]{element.rawname});
      }
      if (getSawFallback(fDepth - 1)) {
        reportFatalError(
            "FallbackChild",
            new Object[]{element.rawname});
      }
      if (getState() == STATE_NORMAL_PROCESSING) {
        if (fResultDepth++ == 0) {
          checkMultipleRootElements();
        }
        if (fDocumentHandler != null) {
          augs = modifyAugmentations(augs);
          attributes = processAttributes(attributes);
          fDocumentHandler.startElement(element, attributes, augs);
        }
      }
    } else if (getState() == STATE_NORMAL_PROCESSING) {
      if (fResultDepth++ == 0) {
        checkMultipleRootElements();
      }
      if (fDocumentHandler != null) {
        augs = modifyAugmentations(augs);
        attributes = processAttributes(attributes);
        fDocumentHandler.startElement(element, attributes, augs);
      }
    }
  }

  @Override
  public void emptyElement(
      QName element,
      XMLAttributes attributes,
      Augmentations augs)
      throws XNIException {
    fDepth++;
    int lastState = getState(fDepth - 1);
    // If the last two states were fallback then this must be a descendant of an include
    // child which isn't a fallback. The specification says we should ignore such elements
    // and their children.
    if (lastState == STATE_EXPECT_FALLBACK && getState(fDepth - 2) == STATE_EXPECT_FALLBACK) {
      setState(STATE_IGNORE);
    } else {
      setState(lastState);
    }

    // we process the xml:base and xml:lang attributes regardless
    // of what type of element it is.
    processXMLBaseAttributes(attributes);
    if (fFixupLanguage) {
      processXMLLangAttributes(attributes);
    }

    if (isIncludeElement(element)) {
      boolean success = this.handleIncludeElement(attributes);
      if (success) {
        setState(STATE_IGNORE);
      } else {
        reportFatalError("NoFallback",
            new Object[]{attributes.getValue(null, "href")});
      }
    } else if (isFallbackElement(element)) {
      this.handleFallbackElement();
    } else if (hasXIncludeNamespace(element)) {
      if (getSawInclude(fDepth - 1)) {
        reportFatalError(
            "IncludeChild",
            new Object[]{element.rawname});
      }
      if (getSawFallback(fDepth - 1)) {
        reportFatalError(
            "FallbackChild",
            new Object[]{element.rawname});
      }
      if (getState() == STATE_NORMAL_PROCESSING) {
        if (fResultDepth == 0) {
          checkMultipleRootElements();
        }
        if (fDocumentHandler != null) {
          augs = modifyAugmentations(augs);
          attributes = processAttributes(attributes);
          fDocumentHandler.emptyElement(element, attributes, augs);
        }
      }
    } else if (getState() == STATE_NORMAL_PROCESSING) {
      if (fResultDepth == 0) {
        checkMultipleRootElements();
      }
      if (fDocumentHandler != null) {
        augs = modifyAugmentations(augs);
        attributes = processAttributes(attributes);
        fDocumentHandler.emptyElement(element, attributes, augs);
      }
    }
    // reset the out of scope stack elements
    setSawFallback(fDepth + 1, false);
    setSawInclude(fDepth, false);

    // check if an xml:base has gone out of scope
    if (fBaseURIScope.size() > 0 && fDepth == fBaseURIScope.peek()) {
      // pop the values from the stack
      restoreBaseURI();
    }
    fDepth--;
  }

  @Override
  public void endElement(QName element, Augmentations augs)
      throws XNIException {

    if (isIncludeElement(element)) {
      // if we're ending an include element, and we were expecting a fallback
      // we check to see if the children of this include element contained a fallback
      if (getState() == STATE_EXPECT_FALLBACK
          && !getSawFallback(fDepth + 1)) {
        reportFatalError("NoFallback",
            new Object[]{"unknown"});
      }
    }
    if (isFallbackElement(element)) {
      // the state would have been set to normal processing if we were expecting the fallback element
      // now that we're done processing it, we should ignore all the other children of the include element
      if (getState() == STATE_NORMAL_PROCESSING) {
        setState(STATE_IGNORE);
      }
    } else if (getState() == STATE_NORMAL_PROCESSING) {
      --fResultDepth;
      if (fDocumentHandler != null) {
        fDocumentHandler.endElement(element, augs);
      }
    }

    // reset the out of scope stack elements
    setSawFallback(fDepth + 1, false);
    setSawInclude(fDepth, false);

    // check if an xml:base has gone out of scope
    if (fBaseURIScope.size() > 0 && fDepth == fBaseURIScope.peek()) {
      // pop the values from the stack
      restoreBaseURI();
    }

    // check if an xml:lang has gone out of scope
    if (fLanguageScope.size() > 0 && fDepth == fLanguageScope.peek()) {
      // pop the language from the stack
      fCurrentLanguage = restoreLanguage();
    }

    fDepth--;
  }

  @Override
  public void startGeneralEntity(
      String name,
      XMLResourceIdentifier resId,
      String encoding,
      Augmentations augs)
      throws XNIException {
    if (getState() == STATE_NORMAL_PROCESSING) {
      if (fResultDepth == 0) {
        if (augs != null && Boolean.TRUE.equals(augs.getItem(Constants.ENTITY_SKIPPED))) {
          reportFatalError("UnexpandedEntityReferenceIllegal");
        }
      } else if (fDocumentHandler != null) {
        fDocumentHandler.startGeneralEntity(name, resId, encoding, augs);
      }
    }
  }

  @Override
  public void textDecl(String version, String encoding, Augmentations augs)
      throws XNIException {
    if (fDocumentHandler != null
        && getState() == STATE_NORMAL_PROCESSING) {
      fDocumentHandler.textDecl(version, encoding, augs);
    }
  }

  @Override
  public void endGeneralEntity(String name, Augmentations augs)
      throws XNIException {
    if (fDocumentHandler != null
        && getState() == STATE_NORMAL_PROCESSING
        && fResultDepth != 0) {
      fDocumentHandler.endGeneralEntity(name, augs);
    }
  }

  @Override
  public void characters(XMLString text, Augmentations augs)
      throws XNIException {
    if (getState() == STATE_NORMAL_PROCESSING) {
      if (fResultDepth == 0) {
        checkWhitespace(text);
      } else if (fDocumentHandler != null) {
        // we need to change the depth like this so that modifyAugmentations() works
        fDepth++;
        augs = modifyAugmentations(augs);
        fDocumentHandler.characters(text, augs);
        fDepth--;
      }
    }
  }

  @Override
  public void ignorableWhitespace(XMLString text, Augmentations augs)
      throws XNIException {
    if (fDocumentHandler != null
        && getState() == STATE_NORMAL_PROCESSING
        && fResultDepth != 0) {
      fDocumentHandler.ignorableWhitespace(text, augs);
    }
  }

  @Override
  public void startCDATA(Augmentations augs) throws XNIException {
    if (fDocumentHandler != null
        && getState() == STATE_NORMAL_PROCESSING
        && fResultDepth != 0) {
      fDocumentHandler.startCDATA(augs);
    }
  }

  @Override
  public void endCDATA(Augmentations augs) throws XNIException {
    if (fDocumentHandler != null
        && getState() == STATE_NORMAL_PROCESSING
        && fResultDepth != 0) {
      fDocumentHandler.endCDATA(augs);
    }
  }

  @Override
  public void endDocument(Augmentations augs) throws XNIException {
    if (isRootDocument()) {
      if (!fSeenRootElement) {
        reportFatalError("RootElementRequired");
      }
      if (fDocumentHandler != null) {
        fDocumentHandler.endDocument(augs);
      }
    }
  }

  @Override
  public void setDocumentSource(XMLDocumentSource source) {
    fDocumentSource = source;
  }

  @Override
  public XMLDocumentSource getDocumentSource() {
    return fDocumentSource;
  }

  // DTDHandler methods
  // We are only interested in the notation and unparsed entity declarations,
  // the rest we just pass on

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#attributeDecl(java.lang.String, java.lang.String, java.lang.String, java.lang.String[], java.lang.String, com.sun.org.apache.xerces.internal.xni.XMLString, com.sun.org.apache.xerces.internal.xni.XMLString, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void attributeDecl(
      String elementName,
      String attributeName,
      String type,
      String[] enumeration,
      String defaultType,
      XMLString defaultValue,
      XMLString nonNormalizedDefaultValue,
      Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.attributeDecl(
          elementName,
          attributeName,
          type,
          enumeration,
          defaultType,
          defaultValue,
          nonNormalizedDefaultValue,
          augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#elementDecl(java.lang.String, java.lang.String, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void elementDecl(
      String name,
      String contentModel,
      Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.elementDecl(name, contentModel, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#endAttlist(com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void endAttlist(Augmentations augmentations) throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.endAttlist(augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#endConditional(com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void endConditional(Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.endConditional(augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#endDTD(com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void endDTD(Augmentations augmentations) throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.endDTD(augmentations);
    }
    fInDTD = false;
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#endExternalSubset(com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void endExternalSubset(Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.endExternalSubset(augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#endParameterEntity(java.lang.String, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void endParameterEntity(String name, Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.endParameterEntity(name, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#externalEntityDecl(java.lang.String, com.sun.org.apache.xerces.internal.xni.XMLResourceIdentifier, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void externalEntityDecl(
      String name,
      XMLResourceIdentifier identifier,
      Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.externalEntityDecl(name, identifier, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#getDTDSource()
   */
  @Override
  public XMLDTDSource getDTDSource() {
    return fDTDSource;
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#ignoredCharacters(com.sun.org.apache.xerces.internal.xni.XMLString, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void ignoredCharacters(XMLString text, Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.ignoredCharacters(text, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#internalEntityDecl(java.lang.String, com.sun.org.apache.xerces.internal.xni.XMLString, com.sun.org.apache.xerces.internal.xni.XMLString, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void internalEntityDecl(
      String name,
      XMLString text,
      XMLString nonNormalizedText,
      Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.internalEntityDecl(
          name,
          text,
          nonNormalizedText,
          augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#notationDecl(java.lang.String, com.sun.org.apache.xerces.internal.xni.XMLResourceIdentifier, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void notationDecl(
      String name,
      XMLResourceIdentifier identifier,
      Augmentations augmentations)
      throws XNIException {
    this.addNotation(name, identifier, augmentations);
    if (fDTDHandler != null) {
      fDTDHandler.notationDecl(name, identifier, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#setDTDSource(com.sun.org.apache.xerces.internal.xni.parser.XMLDTDSource)
   */
  @Override
  public void setDTDSource(XMLDTDSource source) {
    fDTDSource = source;
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#startAttlist(java.lang.String, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void startAttlist(String elementName, Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.startAttlist(elementName, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#startConditional(short, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void startConditional(short type, Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.startConditional(type, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#startDTD(com.sun.org.apache.xerces.internal.xni.XMLLocator, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void startDTD(XMLLocator locator, Augmentations augmentations)
      throws XNIException {
    fInDTD = true;
    if (fDTDHandler != null) {
      fDTDHandler.startDTD(locator, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#startExternalSubset(com.sun.org.apache.xerces.internal.xni.XMLResourceIdentifier, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void startExternalSubset(
      XMLResourceIdentifier identifier,
      Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.startExternalSubset(identifier, augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#startParameterEntity(java.lang.String, com.sun.org.apache.xerces.internal.xni.XMLResourceIdentifier, java.lang.String, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void startParameterEntity(
      String name,
      XMLResourceIdentifier identifier,
      String encoding,
      Augmentations augmentations)
      throws XNIException {
    if (fDTDHandler != null) {
      fDTDHandler.startParameterEntity(
          name,
          identifier,
          encoding,
          augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.XMLDTDHandler#unparsedEntityDecl(java.lang.String, com.sun.org.apache.xerces.internal.xni.XMLResourceIdentifier, java.lang.String, com.sun.org.apache.xerces.internal.xni.Augmentations)
   */
  @Override
  public void unparsedEntityDecl(
      String name,
      XMLResourceIdentifier identifier,
      String notation,
      Augmentations augmentations)
      throws XNIException {
    this.addUnparsedEntity(name, identifier, notation, augmentations);
    if (fDTDHandler != null) {
      fDTDHandler.unparsedEntityDecl(
          name,
          identifier,
          notation,
          augmentations);
    }
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.parser.XMLDTDSource#getDTDHandler()
   */
  @Override
  public XMLDTDHandler getDTDHandler() {
    return fDTDHandler;
  }

  /* (non-Javadoc)
   * @see com.sun.org.apache.xerces.internal.xni.parser.XMLDTDSource#setDTDHandler(com.sun.org.apache.xerces.internal.xni.XMLDTDHandler)
   */
  @Override
  public void setDTDHandler(XMLDTDHandler handler) {
    fDTDHandler = handler;
  }

  // XIncludeHandler methods

  private void setErrorReporter(XMLErrorReporter reporter) {
    fErrorReporter = reporter;
    if (fErrorReporter != null) {
      fErrorReporter.putMessageFormatter(
          XIncludeMessageFormatter.XINCLUDE_DOMAIN, fXIncludeMessageFormatter);
      // this ensures the proper location is displayed in error messages
      if (fDocLocation != null) {
        fErrorReporter.setDocumentLocator(fDocLocation);
      }
    }
  }

  protected void handleFallbackElement() {
    if (!getSawInclude(fDepth - 1)) {
      if (getState() == STATE_IGNORE) {
        return;
      }
      reportFatalError("FallbackParent");
    }

    setSawInclude(fDepth, false);
    fNamespaceContext.setContextInvalid();

    if (getSawFallback(fDepth)) {
      reportFatalError("MultipleFallbacks");
    } else {
      setSawFallback(fDepth, true);
    }

    // Either the state is STATE_EXPECT_FALLBACK or it's STATE_IGNORE.
    // If we're ignoring, we want to stay ignoring. But if we're expecting this fallback element,
    // we want to signal that we should process the children.
    if (getState() == STATE_EXPECT_FALLBACK) {
      setState(STATE_NORMAL_PROCESSING);
    }
  }

  protected boolean handleIncludeElement(XMLAttributes attributes)
      throws XNIException {
    if (getSawInclude(fDepth - 1)) {
      reportFatalError("IncludeChild", new Object[]{XINCLUDE_INCLUDE});
    }
    if (getState() == STATE_IGNORE) {
      return true;
    }
    setSawInclude(fDepth, true);
    fNamespaceContext.setContextInvalid();

    // TODO: does Java use IURIs by default?
    //       [Definition: An internationalized URI reference, or IURI, is a URI reference that directly uses [Unicode] characters.]
    // TODO: figure out what section 4.1.1 of the XInclude spec is talking about
    //       has to do with disallowed ASCII character escaping
    //       this ties in with the above IURI section, but I suspect Java already does it

    String href = attributes.getValue(XINCLUDE_ATTR_HREF);
    String parse = attributes.getValue(XINCLUDE_ATTR_PARSE);
    String xpointer = attributes.getValue(XPOINTER);
    String accept = attributes.getValue(XINCLUDE_ATTR_ACCEPT);
    String acceptLanguage = attributes.getValue(XINCLUDE_ATTR_ACCEPT_LANGUAGE);

    if (parse == null) {
      parse = XINCLUDE_PARSE_XML;
    }
    if (href == null) {
      href = XMLSymbols.EMPTY_STRING;
    }
    if (href.length() == 0 && XINCLUDE_PARSE_XML.equals(parse)) {
      if (xpointer == null) {
        reportFatalError("XpointerMissing");
      } else {
        // When parse="xml" and an xpointer is specified treat
        // all absences of the href attribute as a resource error.
        Locale locale = (fErrorReporter != null) ? fErrorReporter.getLocale() : null;
        String reason = fXIncludeMessageFormatter
            .formatMessage(locale, "XPointerStreamability", null);
        reportResourceError("XMLResourceError", new Object[]{href, reason});
        return false;
      }
    }

    URI hrefURI = null;

    // Check whether href is correct and perform escaping as per section 4.1.1 of the XInclude spec.
    // Report fatal error if the href value contains a fragment identifier or if the value after
    // escaping is a syntactically invalid URI or IRI.
    try {
      hrefURI = new URI(href, true);
      if (hrefURI.getFragment() != null) {
        reportFatalError("HrefFragmentIdentifierIllegal", new Object[]{href});
      }
    } catch (URI.MalformedURIException exc) {
      String newHref = escapeHref(href);
      if (href != newHref) {
        href = newHref;
        try {
          hrefURI = new URI(href, true);
          if (hrefURI.getFragment() != null) {
            reportFatalError("HrefFragmentIdentifierIllegal", new Object[]{href});
          }
        } catch (URI.MalformedURIException exc2) {
          reportFatalError("HrefSyntacticallyInvalid", new Object[]{href});
        }
      } else {
        reportFatalError("HrefSyntacticallyInvalid", new Object[]{href});
      }
    }

    // Verify that if an accept and/or an accept-language attribute exist
    // that the value(s) don't contain disallowed characters.
    if (accept != null && !isValidInHTTPHeader(accept)) {
      reportFatalError("AcceptMalformed", null);
      accept = null;
    }
    if (acceptLanguage != null && !isValidInHTTPHeader(acceptLanguage)) {
      reportFatalError("AcceptLanguageMalformed", null);
      acceptLanguage = null;
    }

    XMLInputSource includedSource = null;
    if (fEntityResolver != null) {
      try {
        XMLResourceIdentifier resourceIdentifier =
            new XMLResourceIdentifierImpl(
                null,
                href,
                fCurrentBaseURI.getExpandedSystemId(),
                XMLEntityManager.expandSystemId(
                    href,
                    fCurrentBaseURI.getExpandedSystemId(),
                    false));

        includedSource =
            fEntityResolver.resolveEntity(resourceIdentifier);

        if (includedSource != null &&
            !(includedSource instanceof HTTPInputSource) &&
            (accept != null || acceptLanguage != null) &&
            includedSource.getCharacterStream() == null &&
            includedSource.getByteStream() == null) {

          includedSource = createInputSource(includedSource.getPublicId(),
              includedSource.getSystemId(),
              includedSource.getBaseSystemId(), accept, acceptLanguage);
        }
      } catch (IOException e) {
        reportResourceError(
            "XMLResourceError",
            new Object[]{href, e.getMessage()});
        return false;
      }
    }

    if (includedSource == null) {
      // setup an HTTPInputSource if either of the content negotation attributes were specified.
      if (accept != null || acceptLanguage != null) {
        includedSource = createInputSource(null, href, fCurrentBaseURI.getExpandedSystemId(),
            accept, acceptLanguage);
      } else {
        includedSource = new XMLInputSource(null, href, fCurrentBaseURI.getExpandedSystemId());
      }
    }

    if (parse.equals(XINCLUDE_PARSE_XML)) {
      // Instead of always creating a new configuration, the first one can be reused
      if ((xpointer != null && fXPointerChildConfig == null)
          || (xpointer == null && fXIncludeChildConfig == null)) {

        String parserName = XINCLUDE_DEFAULT_CONFIGURATION;
        if (xpointer != null) {
          parserName = "com.sun.org.apache.xerces.internal.parsers.XPointerParserConfiguration";
        }

        fChildConfig =
            (XMLParserConfiguration) ObjectFactory.newInstance(
                parserName,
                true);

        // use the same symbol table, error reporter, entity resolver, security manager and buffer size.
        if (fSymbolTable != null) {
          fChildConfig.setProperty(SYMBOL_TABLE, fSymbolTable);
        }
        if (fErrorReporter != null) {
          fChildConfig.setProperty(ERROR_REPORTER, fErrorReporter);
        }
        if (fEntityResolver != null) {
          fChildConfig.setProperty(ENTITY_RESOLVER, fEntityResolver);
        }
        fChildConfig.setProperty(SECURITY_MANAGER, fSecurityManager);
        fChildConfig.setProperty(XML_SECURITY_PROPERTY_MANAGER, fSecurityPropertyMgr);
        fChildConfig.setProperty(BUFFER_SIZE, new Integer(fBufferSize));

        // features must be copied to child configuration
        fNeedCopyFeatures = true;

        // use the same namespace context
        fChildConfig.setProperty(
            Constants.XERCES_PROPERTY_PREFIX
                + Constants.NAMESPACE_CONTEXT_PROPERTY,
            fNamespaceContext);

        fChildConfig.setFeature(
            XINCLUDE_FIXUP_BASE_URIS,
            fFixupBaseURIs);

        fChildConfig.setFeature(
            XINCLUDE_FIXUP_LANGUAGE,
            fFixupLanguage);

        // If the xpointer attribute is present
        if (xpointer != null) {

          XPointerHandler newHandler =
              (XPointerHandler) fChildConfig.getProperty(
                  Constants.XERCES_PROPERTY_PREFIX
                      + Constants.XPOINTER_HANDLER_PROPERTY);

          fXPtrProcessor = newHandler;

          // ???
          ((XPointerHandler) fXPtrProcessor).setProperty(
              Constants.XERCES_PROPERTY_PREFIX
                  + Constants.NAMESPACE_CONTEXT_PROPERTY,
              fNamespaceContext);

          ((XPointerHandler) fXPtrProcessor).setProperty(XINCLUDE_FIXUP_BASE_URIS,
              fFixupBaseURIs);

          ((XPointerHandler) fXPtrProcessor).setProperty(
              XINCLUDE_FIXUP_LANGUAGE, fFixupLanguage);

          if (fErrorReporter != null) {
            ((XPointerHandler) fXPtrProcessor).setProperty(ERROR_REPORTER, fErrorReporter);
          }
          // ???

          newHandler.setParent(this);
          newHandler.setDocumentHandler(this.getDocumentHandler());
          fXPointerChildConfig = fChildConfig;
        } else {
          XIncludeHandler newHandler =
              (XIncludeHandler) fChildConfig.getProperty(
                  Constants.XERCES_PROPERTY_PREFIX
                      + Constants.XINCLUDE_HANDLER_PROPERTY);

          newHandler.setParent(this);
          newHandler.setDocumentHandler(this.getDocumentHandler());
          fXIncludeChildConfig = fChildConfig;
        }
      }

      // If an xpointer attribute is present
      if (xpointer != null) {
        fChildConfig = fXPointerChildConfig;

        // Parse the XPointer expression
        try {
          ((XPointerProcessor) fXPtrProcessor).parseXPointer(xpointer);

        } catch (XNIException ex) {
          // report the XPointer error as a resource error
          reportResourceError(
              "XMLResourceError",
              new Object[]{href, ex.getMessage()});
          return false;
        }
      } else {
        fChildConfig = fXIncludeChildConfig;
      }

      // set all features on parserConfig to match this parser configuration
      if (fNeedCopyFeatures) {
        copyFeatures(fSettings, fChildConfig);
      }
      fNeedCopyFeatures = false;

      try {
        fNamespaceContext.pushScope();

        fChildConfig.parse(includedSource);
        // necessary to make sure proper location is reported in errors
        if (fErrorReporter != null) {
          fErrorReporter.setDocumentLocator(fDocLocation);
        }

        // If the xpointer attribute is present
        if (xpointer != null) {
          // and it was not resolved
          if (!((XPointerProcessor) fXPtrProcessor).isXPointerResolved()) {
            Locale locale = (fErrorReporter != null) ? fErrorReporter.getLocale() : null;
            String reason = fXIncludeMessageFormatter
                .formatMessage(locale, "XPointerResolutionUnsuccessful", null);
            reportResourceError("XMLResourceError", new Object[]{href, reason});
            // use the fallback
            return false;
          }
        }
      } catch (XNIException e) {
        // necessary to make sure proper location is reported in errors
        if (fErrorReporter != null) {
          fErrorReporter.setDocumentLocator(fDocLocation);
        }
        reportFatalError("XMLParseError", new Object[]{href, e.getMessage()});
      } catch (IOException e) {
        // necessary to make sure proper location is reported in errors
        if (fErrorReporter != null) {
          fErrorReporter.setDocumentLocator(fDocLocation);
        }
        // An IOException indicates that we had trouble reading the file, not
        // that it was an invalid XML file.  So we send a resource error, not a
        // fatal error.
        reportResourceError(
            "XMLResourceError",
            new Object[]{href, e.getMessage()});
        return false;
      } finally {
        fNamespaceContext.popScope();
      }
    } else if (parse.equals(XINCLUDE_PARSE_TEXT)) {
      // we only care about encoding for parse="text"
      String encoding = attributes.getValue(XINCLUDE_ATTR_ENCODING);
      includedSource.setEncoding(encoding);
      XIncludeTextReader textReader = null;

      try {
        // Setup the appropriate text reader.
        if (!fIsXML11) {
          if (fXInclude10TextReader == null) {
            fXInclude10TextReader = new XIncludeTextReader(includedSource, this, fBufferSize);
          } else {
            fXInclude10TextReader.setInputSource(includedSource);
          }
          textReader = fXInclude10TextReader;
        } else {
          if (fXInclude11TextReader == null) {
            fXInclude11TextReader = new XInclude11TextReader(includedSource, this, fBufferSize);
          } else {
            fXInclude11TextReader.setInputSource(includedSource);
          }
          textReader = fXInclude11TextReader;
        }
        textReader.setErrorReporter(fErrorReporter);
        textReader.parse();
      }
      // encoding errors
      catch (MalformedByteSequenceException ex) {
        fErrorReporter.reportError(ex.getDomain(), ex.getKey(),
            ex.getArguments(), XMLErrorReporter.SEVERITY_FATAL_ERROR);
      } catch (CharConversionException e) {
        fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN,
            "CharConversionFailure", null, XMLErrorReporter.SEVERITY_FATAL_ERROR);
      } catch (IOException e) {
        reportResourceError(
            "TextResourceError",
            new Object[]{href, e.getMessage()});
        return false;
      } finally {
        if (textReader != null) {
          try {
            textReader.close();
          } catch (IOException e) {
            reportResourceError(
                "TextResourceError",
                new Object[]{href, e.getMessage()});
            return false;
          }
        }
      }
    } else {
      reportFatalError("InvalidParseValue", new Object[]{parse});
    }
    return true;
  }

  /**
   * Returns true if the element has the namespace "http://www.w3.org/2001/XInclude"
   *
   * @param element the element to check
   * @return true if the element has the namespace "http://www.w3.org/2001/XInclude"
   */
  protected boolean hasXIncludeNamespace(QName element) {
    // REVISIT: The namespace of this element should be bound
    // already. Why are we looking it up from the namespace
    // context? -- mrglavas
    return element.uri == XINCLUDE_NS_URI
        || fNamespaceContext.getURI(element.prefix) == XINCLUDE_NS_URI;
  }

  /**
   * Checks if the element is an &lt;include&gt; element.  The element must have
   * the XInclude namespace, and a local name of "include".
   *
   * @param element the element to check
   * @return true if the element is an &lt;include&gt; element
   * @see #hasXIncludeNamespace(QName)
   */
  protected boolean isIncludeElement(QName element) {
    return element.localpart.equals(XINCLUDE_INCLUDE) &&
        hasXIncludeNamespace(element);
  }

  /**
   * Checks if the element is an &lt;fallback&gt; element.  The element must have
   * the XInclude namespace, and a local name of "fallback".
   *
   * @param element the element to check
   * @return true if the element is an &lt;fallback; element
   * @see #hasXIncludeNamespace(QName)
   */
  protected boolean isFallbackElement(QName element) {
    return element.localpart.equals(XINCLUDE_FALLBACK) &&
        hasXIncludeNamespace(element);
  }

  /**
   * Returns true if the current [base URI] is the same as the [base URI] that
   * was in effect on the include parent.  This method should <em>only</em> be called
   * when the current element is a top level included element, i.e. the direct child
   * of a fallback element, or the root elements in an included document.
   * The "include parent" is the element which, in the result infoset, will be the
   * direct parent of the current element.
   *
   * @return true if the [base URIs] are the same string
   */
  protected boolean sameBaseURIAsIncludeParent() {
    String parentBaseURI = getIncludeParentBaseURI();
    String baseURI = fCurrentBaseURI.getExpandedSystemId();
    // REVISIT: should we use File#sameFile() ?
    //          I think the benefit of using it is that it resolves host names
    //          instead of just doing a string comparison.
    // TODO: [base URI] is still an open issue with the working group.
    //       They're deciding if xml:base should be added if the [base URI] is different in terms
    //       of resolving relative references, or if it should be added if they are different at all.
    //       Revisit this after a final decision has been made.
    //       The decision also affects whether we output the file name of the URI, or just the path.
    return parentBaseURI != null && parentBaseURI.equals(baseURI);
  }

  /**
   * Returns true if the current [language] is equivalent to the [language] that
   * was in effect on the include parent, taking case-insensitivity into account
   * as per [RFC 3066].  This method should <em>only</em> be called when the
   * current element is a top level included element, i.e. the direct child
   * of a fallback element, or the root elements in an included document.
   * The "include parent" is the element which, in the result infoset, will be the
   * direct parent of the current element.
   *
   * @return true if the [language] properties have the same value taking case-insensitivity into
   * account as per [RFC 3066].
   */
  protected boolean sameLanguageAsIncludeParent() {
    String parentLanguage = getIncludeParentLanguage();
    return parentLanguage != null && parentLanguage.equalsIgnoreCase(fCurrentLanguage);
  }

  /**
   * Checks if the file indicated by the given XMLLocator has already been included
   * in the current stack.
   *
   * @param includedSource the source to check for inclusion
   * @return true if the source has already been included
   */
  protected boolean searchForRecursiveIncludes(XMLLocator includedSource) {
    String includedSystemId = includedSource.getExpandedSystemId();

    if (includedSystemId == null) {
      try {
        includedSystemId =
            XMLEntityManager.expandSystemId(
                includedSource.getLiteralSystemId(),
                includedSource.getBaseSystemId(),
                false);
      } catch (MalformedURIException e) {
        reportFatalError("ExpandedSystemId");
      }
    }

    if (includedSystemId.equals(fCurrentBaseURI.getExpandedSystemId())) {
      return true;
    }

    if (fParentXIncludeHandler == null) {
      return false;
    }
    return fParentXIncludeHandler.searchForRecursiveIncludes(
        includedSource);
  }

  /**
   * Returns true if the current element is a top level included item.  This means
   * it's either the child of a fallback element, or the top level item in an
   * included document
   *
   * @return true if the current element is a top level included item
   */
  protected boolean isTopLevelIncludedItem() {
    return isTopLevelIncludedItemViaInclude()
        || isTopLevelIncludedItemViaFallback();
  }

  protected boolean isTopLevelIncludedItemViaInclude() {
    return fDepth == 1 && !isRootDocument();
  }

  protected boolean isTopLevelIncludedItemViaFallback() {
    // Technically, this doesn't check if the parent was a fallback, it also
    // would return true if any of the parent's sibling elements were fallbacks.
    // However, this doesn't matter, since we will always be ignoring elements
    // whose parent's siblings were fallbacks.
    return getSawFallback(fDepth - 1);
  }

  /**
   * Processes the XMLAttributes object of startElement() calls.  Performs the following tasks:
   * <ul>
   * <li> If the element is a top level included item whose [base URI] is different from the
   * [base URI] of the include parent, then an xml:base attribute is added to specify the
   * true [base URI]
   * <li> For all namespace prefixes which are in-scope in an included item, but not in scope
   * in the include parent, a xmlns:prefix attribute is added
   * <li> For all attributes with a type of ENTITY, ENTITIES or NOTATIONS, the notations and
   * unparsed entities are processed as described in the spec, sections 4.5.1 and 4.5.2
   * </ul>
   */
  protected XMLAttributes processAttributes(XMLAttributes attributes) {
    if (isTopLevelIncludedItem()) {
      // Modify attributes to fix the base URI (spec 4.5.5).
      // We only do it to top level included elements, which have a different
      // base URI than their include parent.
      if (fFixupBaseURIs && !sameBaseURIAsIncludeParent()) {
        if (attributes == null) {
          attributes = new XMLAttributesImpl();
        }

        // This causes errors with schema validation, if the schema doesn't
        // specify that these elements can have an xml:base attribute
        String uri = null;
        try {
          uri = this.getRelativeBaseURI();
        } catch (MalformedURIException e) {
          // this shouldn't ever happen, since by definition, we had to traverse
          // the same URIs to even get to this place
          uri = fCurrentBaseURI.getExpandedSystemId();
        }
        int index =
            attributes.addAttribute(
                XML_BASE_QNAME,
                XMLSymbols.fCDATASymbol,
                uri);
        attributes.setSpecified(index, true);
      }

      // Modify attributes to perform language-fixup (spec 4.5.6).
      // We only do it to top level included elements, which have a different
      // [language] than their include parent.
      if (fFixupLanguage && !sameLanguageAsIncludeParent()) {
        if (attributes == null) {
          attributes = new XMLAttributesImpl();
        }
        int index =
            attributes.addAttribute(
                XML_LANG_QNAME,
                XMLSymbols.fCDATASymbol,
                fCurrentLanguage);
        attributes.setSpecified(index, true);
      }

      // Modify attributes of included items to do namespace-fixup. (spec 4.5.4)
      Enumeration inscopeNS = fNamespaceContext.getAllPrefixes();
      while (inscopeNS.hasMoreElements()) {
        String prefix = (String) inscopeNS.nextElement();
        String parentURI =
            fNamespaceContext.getURIFromIncludeParent(prefix);
        String uri = fNamespaceContext.getURI(prefix);
        if (parentURI != uri && attributes != null) {
          if (prefix == XMLSymbols.EMPTY_STRING) {
            if (attributes
                .getValue(
                    NamespaceContext.XMLNS_URI,
                    XMLSymbols.PREFIX_XMLNS)
                == null) {
              if (attributes == null) {
                attributes = new XMLAttributesImpl();
              }

              QName ns = (QName) NEW_NS_ATTR_QNAME.clone();
              ns.prefix = null;
              ns.localpart = XMLSymbols.PREFIX_XMLNS;
              ns.rawname = XMLSymbols.PREFIX_XMLNS;
              int index =
                  attributes.addAttribute(
                      ns,
                      XMLSymbols.fCDATASymbol,
                      uri != null ? uri : XMLSymbols.EMPTY_STRING);
              attributes.setSpecified(index, true);
              // Need to re-declare this prefix in the current context
              // in order for the SAX parser to report the appropriate
              // start and end prefix mapping events. -- mrglavas
              fNamespaceContext.declarePrefix(prefix, uri);
            }
          } else if (
              attributes.getValue(NamespaceContext.XMLNS_URI, prefix)
                  == null) {
            if (attributes == null) {
              attributes = new XMLAttributesImpl();
            }

            QName ns = (QName) NEW_NS_ATTR_QNAME.clone();
            ns.localpart = prefix;
            ns.rawname += prefix;
            ns.rawname = (fSymbolTable != null) ?
                fSymbolTable.addSymbol(ns.rawname) :
                ns.rawname.intern();
            int index =
                attributes.addAttribute(
                    ns,
                    XMLSymbols.fCDATASymbol,
                    uri != null ? uri : XMLSymbols.EMPTY_STRING);
            attributes.setSpecified(index, true);
            // Need to re-declare this prefix in the current context
            // in order for the SAX parser to report the appropriate
            // start and end prefix mapping events. -- mrglavas
            fNamespaceContext.declarePrefix(prefix, uri);
          }
        }
      }
    }

    if (attributes != null) {
      int length = attributes.getLength();
      for (int i = 0; i < length; i++) {
        String type = attributes.getType(i);
        String value = attributes.getValue(i);
        if (type == XMLSymbols.fENTITYSymbol) {
          this.checkUnparsedEntity(value);
        }
        if (type == XMLSymbols.fENTITIESSymbol) {
          // 4.5.1 - Unparsed Entities
          StringTokenizer st = new StringTokenizer(value);
          while (st.hasMoreTokens()) {
            String entName = st.nextToken();
            this.checkUnparsedEntity(entName);
          }
        } else if (type == XMLSymbols.fNOTATIONSymbol) {
          // 4.5.2 - Notations
          this.checkNotation(value);
        }
                /* We actually don't need to do anything for 4.5.3, because at this stage the
                 * value of the attribute is just a string. It will be taken care of later
                 * in the pipeline, when the IDREFs are actually resolved against IDs.
                 *
                 * if (type == XMLSymbols.fIDREFSymbol || type == XMLSymbols.fIDREFSSymbol) { }
                 */
      }
    }

    return attributes;
  }

  /**
   * Returns a URI, relative to the include parent's base URI, of the current
   * [base URI].  For instance, if the current [base URI] was "dir1/dir2/file.xml"
   * and the include parent's [base URI] was "dir/", this would return "dir2/file.xml".
   *
   * @return the relative URI
   */
  protected String getRelativeBaseURI() throws MalformedURIException {
    int includeParentDepth = getIncludeParentDepth();
    String relativeURI = this.getRelativeURI(includeParentDepth);
    if (isRootDocument()) {
      return relativeURI;
    } else {
      if (relativeURI.equals("")) {
        relativeURI = fCurrentBaseURI.getLiteralSystemId();
      }

      if (includeParentDepth == 0) {
        if (fParentRelativeURI == null) {
          fParentRelativeURI =
              fParentXIncludeHandler.getRelativeBaseURI();
        }
        if (fParentRelativeURI.equals("")) {
          return relativeURI;
        }

        URI base = new URI(fParentRelativeURI, true);
        URI uri = new URI(base, relativeURI);

        /** Check whether the scheme components are equal. */
        final String baseScheme = base.getScheme();
        final String literalScheme = uri.getScheme();
        if (!Objects.equals(baseScheme, literalScheme)) {
          return relativeURI;
        }

        /** Check whether the authority components are equal. */
        final String baseAuthority = base.getAuthority();
        final String literalAuthority = uri.getAuthority();
        if (!Objects.equals(baseAuthority, literalAuthority)) {
          return uri.getSchemeSpecificPart();
        }

        /**
         * The scheme and authority components are equal,
         * return the path and the possible query and/or
         * fragment which follow.
         */
        final String literalPath = uri.getPath();
        final String literalQuery = uri.getQueryString();
        final String literalFragment = uri.getFragment();
        if (literalQuery != null || literalFragment != null) {
          final StringBuilder buffer = new StringBuilder();
          if (literalPath != null) {
            buffer.append(literalPath);
          }
          if (literalQuery != null) {
            buffer.append('?');
            buffer.append(literalQuery);
          }
          if (literalFragment != null) {
            buffer.append('#');
            buffer.append(literalFragment);
          }
          return buffer.toString();
        }
        return literalPath;
      } else {
        return relativeURI;
      }
    }
  }

  /**
   * Returns the [base URI] of the include parent.
   *
   * @return the base URI of the include parent.
   */
  private String getIncludeParentBaseURI() {
    int depth = getIncludeParentDepth();
    if (!isRootDocument() && depth == 0) {
      return fParentXIncludeHandler.getIncludeParentBaseURI();
    } else {
      return this.getBaseURI(depth);
    }
  }

  /**
   * Returns the [language] of the include parent.
   *
   * @return the language property of the include parent.
   */
  private String getIncludeParentLanguage() {
    int depth = getIncludeParentDepth();
    if (!isRootDocument() && depth == 0) {
      return fParentXIncludeHandler.getIncludeParentLanguage();
    } else {
      return getLanguage(depth);
    }
  }

  /**
   * Returns the depth of the include parent.  Here, the include parent is
   * calculated as the last non-include or non-fallback element. It is assumed
   * this method is called when the current element is a top level included item.
   * Returning 0 indicates that the top level element in this document
   * was an include element.
   *
   * @return the depth of the top level include element
   */
  private int getIncludeParentDepth() {
    // We don't start at fDepth, since it is either the top level included item,
    // or an include element, when this method is called.
    for (int i = fDepth - 1; i >= 0; i--) {
      // This technically might not always return the first non-include/fallback
      // element that it comes to, since sawFallback() returns true if a fallback
      // was ever encountered at that depth.  However, if a fallback was encountered
      // at that depth, and it wasn't the direct descendant of the current element
      // then we can't be in a situation where we're calling this method (because
      // we'll always be in STATE_IGNORE)
      if (!getSawInclude(i) && !getSawFallback(i)) {
        return i;
      }
    }
    // shouldn't get here, since depth 0 should never have an include element or
    // a fallback element
    return 0;
  }

  /**
   * Returns the current element depth of the result infoset.
   */
  private int getResultDepth() {
    return fResultDepth;
  }

  /**
   * Modify the augmentations.  Add an [included] infoset item, if the current
   * element is a top level included item.
   *
   * @param augs the Augmentations to modify.
   * @return the modified Augmentations
   */
  protected Augmentations modifyAugmentations(Augmentations augs) {
    return modifyAugmentations(augs, false);
  }

  /**
   * Modify the augmentations.  Add an [included] infoset item, if <code>force</code>
   * is true, or if the current element is a top level included item.
   *
   * @param augs the Augmentations to modify.
   * @param force whether to force modification
   * @return the modified Augmentations
   */
  protected Augmentations modifyAugmentations(
      Augmentations augs,
      boolean force) {
    if (force || isTopLevelIncludedItem()) {
      if (augs == null) {
        augs = new AugmentationsImpl();
      }
      augs.putItem(XINCLUDE_INCLUDED, Boolean.TRUE);
    }
    return augs;
  }

  protected int getState(int depth) {
    return fState[depth];
  }

  protected int getState() {
    return fState[fDepth];
  }

  protected void setState(int state) {
    if (fDepth >= fState.length) {
      int[] newarray = new int[fDepth * 2];
      System.arraycopy(fState, 0, newarray, 0, fState.length);
      fState = newarray;
    }
    fState[fDepth] = state;
  }

  /**
   * Records that an &lt;fallback&gt; was encountered at the specified depth,
   * as an ancestor of the current element, or as a sibling of an ancestor of the
   * current element.
   */
  protected void setSawFallback(int depth, boolean val) {
    if (depth >= fSawFallback.length) {
      boolean[] newarray = new boolean[depth * 2];
      System.arraycopy(fSawFallback, 0, newarray, 0, fSawFallback.length);
      fSawFallback = newarray;
    }
    fSawFallback[depth] = val;
  }

  /**
   * Returns whether an &lt;fallback&gt; was encountered at the specified depth,
   * as an ancestor of the current element, or as a sibling of an ancestor of the
   * current element.
   */
  protected boolean getSawFallback(int depth) {
    if (depth >= fSawFallback.length) {
      return false;
    }
    return fSawFallback[depth];
  }

  /**
   * Records that an &lt;include&gt; was encountered at the specified depth,
   * as an ancestor of the current item.
   */
  protected void setSawInclude(int depth, boolean val) {
    if (depth >= fSawInclude.length) {
      boolean[] newarray = new boolean[depth * 2];
      System.arraycopy(fSawInclude, 0, newarray, 0, fSawInclude.length);
      fSawInclude = newarray;
    }
    fSawInclude[depth] = val;
  }

  /**
   * Return whether an &lt;include&gt; was encountered at the specified depth,
   * as an ancestor of the current item.
   */
  protected boolean getSawInclude(int depth) {
    if (depth >= fSawInclude.length) {
      return false;
    }
    return fSawInclude[depth];
  }

  protected void reportResourceError(String key) {
    this.reportFatalError(key, null);
  }

  protected void reportResourceError(String key, Object[] args) {
    this.reportError(key, args, XMLErrorReporter.SEVERITY_WARNING);
  }

  protected void reportFatalError(String key) {
    this.reportFatalError(key, null);
  }

  protected void reportFatalError(String key, Object[] args) {
    this.reportError(key, args, XMLErrorReporter.SEVERITY_FATAL_ERROR);
  }

  private void reportError(String key, Object[] args, short severity) {
    if (fErrorReporter != null) {
      fErrorReporter.reportError(
          XIncludeMessageFormatter.XINCLUDE_DOMAIN,
          key,
          args,
          severity);
    }
    // we won't worry about when error reporter is null, since there should always be
    // at least the default error reporter
  }

  /**
   * Set the parent of this XIncludeHandler in the tree
   */
  protected void setParent(XIncludeHandler parent) {
    fParentXIncludeHandler = parent;
  }

  // used to know whether to pass declarations to the document handler
  protected boolean isRootDocument() {
    return fParentXIncludeHandler == null;
  }

  /**
   * Caches an unparsed entity.
   *
   * @param name the name of the unparsed entity
   * @param identifier the location of the unparsed entity
   * @param augmentations any Augmentations that were on the original unparsed entity declaration
   */
  protected void addUnparsedEntity(
      String name,
      XMLResourceIdentifier identifier,
      String notation,
      Augmentations augmentations) {
    UnparsedEntity ent = new UnparsedEntity();
    ent.name = name;
    ent.systemId = identifier.getLiteralSystemId();
    ent.publicId = identifier.getPublicId();
    ent.baseURI = identifier.getBaseSystemId();
    ent.expandedSystemId = identifier.getExpandedSystemId();
    ent.notation = notation;
    ent.augmentations = augmentations;
    fUnparsedEntities.add(ent);
  }

  /**
   * Caches a notation.
   *
   * @param name the name of the notation
   * @param identifier the location of the notation
   * @param augmentations any Augmentations that were on the original notation declaration
   */
  protected void addNotation(
      String name,
      XMLResourceIdentifier identifier,
      Augmentations augmentations) {
    Notation not = new Notation();
    not.name = name;
    not.systemId = identifier.getLiteralSystemId();
    not.publicId = identifier.getPublicId();
    not.baseURI = identifier.getBaseSystemId();
    not.expandedSystemId = identifier.getExpandedSystemId();
    not.augmentations = augmentations;
    fNotations.add(not);
  }

  /**
   * Checks if an UnparsedEntity with the given name was declared in the DTD of the document
   * for the current pipeline.  If so, then the notation for the UnparsedEntity is checked.
   * If that turns out okay, then the UnparsedEntity is passed to the root pipeline to
   * be checked for conflicts, and sent to the root DTDHandler.
   *
   * @param entName the name of the UnparsedEntity to check
   */
  protected void checkUnparsedEntity(String entName) {
    UnparsedEntity ent = new UnparsedEntity();
    ent.name = entName;
    int index = fUnparsedEntities.indexOf(ent);
    if (index != -1) {
      ent = (UnparsedEntity) fUnparsedEntities.get(index);
      // first check the notation of the unparsed entity
      checkNotation(ent.notation);
      checkAndSendUnparsedEntity(ent);
    }
  }

  /**
   * Checks if a Notation with the given name was declared in the DTD of the document
   * for the current pipeline.  If so, that Notation is passed to the root pipeline to
   * be checked for conflicts, and sent to the root DTDHandler
   *
   * @param notName the name of the Notation to check
   */
  protected void checkNotation(String notName) {
    Notation not = new Notation();
    not.name = notName;
    int index = fNotations.indexOf(not);
    if (index != -1) {
      not = (Notation) fNotations.get(index);
      checkAndSendNotation(not);
    }
  }

  /**
   * The purpose of this method is to check if an UnparsedEntity conflicts with a previously
   * declared entity in the current pipeline stack.  If there is no conflict, the
   * UnparsedEntity is sent by the root pipeline.
   *
   * @param ent the UnparsedEntity to check for conflicts
   */
  protected void checkAndSendUnparsedEntity(UnparsedEntity ent) {
    if (isRootDocument()) {
      int index = fUnparsedEntities.indexOf(ent);
      if (index == -1) {
        // There is no unparsed entity with the same name that we have sent.
        // Calling unparsedEntityDecl() will add the entity to our local store,
        // and also send the unparsed entity to the DTDHandler
        XMLResourceIdentifier id =
            new XMLResourceIdentifierImpl(
                ent.publicId,
                ent.systemId,
                ent.baseURI,
                ent.expandedSystemId);
        addUnparsedEntity(
            ent.name,
            id,
            ent.notation,
            ent.augmentations);
        if (fSendUEAndNotationEvents && fDTDHandler != null) {
          fDTDHandler.unparsedEntityDecl(
              ent.name,
              id,
              ent.notation,
              ent.augmentations);
        }
      } else {
        UnparsedEntity localEntity =
            (UnparsedEntity) fUnparsedEntities.get(index);
        if (!ent.isDuplicate(localEntity)) {
          reportFatalError(
              "NonDuplicateUnparsedEntity",
              new Object[]{ent.name});
        }
      }
    } else {
      fParentXIncludeHandler.checkAndSendUnparsedEntity(ent);
    }
  }

  /**
   * The purpose of this method is to check if a Notation conflicts with a previously
   * declared notation in the current pipeline stack.  If there is no conflict, the
   * Notation is sent by the root pipeline.
   *
   * @param not the Notation to check for conflicts
   */
  protected void checkAndSendNotation(Notation not) {
    if (isRootDocument()) {
      int index = fNotations.indexOf(not);
      if (index == -1) {
        // There is no notation with the same name that we have sent.
        XMLResourceIdentifier id =
            new XMLResourceIdentifierImpl(
                not.publicId,
                not.systemId,
                not.baseURI,
                not.expandedSystemId);
        addNotation(not.name, id, not.augmentations);
        if (fSendUEAndNotationEvents && fDTDHandler != null) {
          fDTDHandler.notationDecl(not.name, id, not.augmentations);
        }
      } else {
        Notation localNotation = (Notation) fNotations.get(index);
        if (!not.isDuplicate(localNotation)) {
          reportFatalError(
              "NonDuplicateNotation",
              new Object[]{not.name});
        }
      }
    } else {
      fParentXIncludeHandler.checkAndSendNotation(not);
    }
  }

  /**
   * Checks whether the string only contains white space characters.
   *
   * @param value the text to check
   */
  private void checkWhitespace(XMLString value) {
    int end = value.offset + value.length;
    for (int i = value.offset; i < end; ++i) {
      if (!XMLChar.isSpace(value.ch[i])) {
        reportFatalError("ContentIllegalAtTopLevel");
        return;
      }
    }
  }

  /**
   * Checks whether the root element has already been processed.
   */
  private void checkMultipleRootElements() {
    if (getRootElementProcessed()) {
      reportFatalError("MultipleRootElements");
    }
    setRootElementProcessed(true);
  }

  /**
   * Sets whether the root element has been processed.
   */
  private void setRootElementProcessed(boolean seenRoot) {
    if (isRootDocument()) {
      fSeenRootElement = seenRoot;
      return;
    }
    fParentXIncludeHandler.setRootElementProcessed(seenRoot);
  }

  /**
   * Returns whether the root element has been processed.
   */
  private boolean getRootElementProcessed() {
    return isRootDocument() ? fSeenRootElement : fParentXIncludeHandler.getRootElementProcessed();
  }

  // It would be nice if we didn't have to repeat code like this, but there's no interface that has
  // setFeature() and addRecognizedFeatures() that the objects have in common.
  protected void copyFeatures(
      XMLComponentManager from,
      ParserConfigurationSettings to) {
    Enumeration features = Constants.getXercesFeatures();
    copyFeatures1(features, Constants.XERCES_FEATURE_PREFIX, from, to);
    features = Constants.getSAXFeatures();
    copyFeatures1(features, Constants.SAX_FEATURE_PREFIX, from, to);
  }

  protected void copyFeatures(
      XMLComponentManager from,
      XMLParserConfiguration to) {
    Enumeration features = Constants.getXercesFeatures();
    copyFeatures1(features, Constants.XERCES_FEATURE_PREFIX, from, to);
    features = Constants.getSAXFeatures();
    copyFeatures1(features, Constants.SAX_FEATURE_PREFIX, from, to);
  }

  private void copyFeatures1(
      Enumeration features,
      String featurePrefix,
      XMLComponentManager from,
      ParserConfigurationSettings to) {
    while (features.hasMoreElements()) {
      String featureId = featurePrefix + (String) features.nextElement();

      to.addRecognizedFeatures(new String[]{featureId});

      try {
        to.setFeature(featureId, from.getFeature(featureId));
      } catch (XMLConfigurationException e) {
        // componentManager doesn't support this feature,
        // so we won't worry about it
      }
    }
  }

  private void copyFeatures1(
      Enumeration features,
      String featurePrefix,
      XMLComponentManager from,
      XMLParserConfiguration to) {
    while (features.hasMoreElements()) {
      String featureId = featurePrefix + (String) features.nextElement();
      boolean value = from.getFeature(featureId);

      try {
        to.setFeature(featureId, value);
      } catch (XMLConfigurationException e) {
        // componentManager doesn't support this feature,
        // so we won't worry about it
      }
    }
  }

  // This is a storage class to hold information about the notations.
  // We're not using XMLNotationDecl because we don't want to lose the augmentations.
  protected static class Notation {

    public String name;
    public String systemId;
    public String baseURI;
    public String publicId;
    public String expandedSystemId;
    public Augmentations augmentations;

    // equals() returns true if two Notations have the same name.
    // Useful for searching Vectors for notations with the same name
    @Override
    public boolean equals(Object obj) {
      return obj == this || obj instanceof Notation
          && Objects.equals(name, ((Notation) obj).name);
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(name);
    }

    // from 4.5.2
    // Notation items with the same [name], [system identifier],
    // [public identifier], and [declaration base URI] are considered
    // to be duplicate. An application may also be able to detect that
    // notations are duplicate through other means. For instance, the URI
    // resulting from combining the system identifier and the declaration
    // base URI is the same.
    public boolean isDuplicate(Object obj) {
      if (obj != null && obj instanceof Notation) {
        Notation other = (Notation) obj;
        return Objects.equals(name, other.name)
            && Objects.equals(publicId, other.publicId)
            && Objects.equals(expandedSystemId, other.expandedSystemId);
      }
      return false;
    }
  }

  // This is a storage class to hold information about the unparsed entities.
  // We're not using XMLEntityDecl because we don't want to lose the augmentations.
  protected static class UnparsedEntity {

    public String name;
    public String systemId;
    public String baseURI;
    public String publicId;
    public String expandedSystemId;
    public String notation;
    public Augmentations augmentations;

    // equals() returns true if two UnparsedEntities have the same name.
    // Useful for searching Vectors for entities with the same name
    @Override
    public boolean equals(Object obj) {
      return obj == this || obj instanceof UnparsedEntity
          && Objects.equals(name, ((UnparsedEntity) obj).name);
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(name);
    }

    // from 4.5.1:
    // Unparsed entity items with the same [name], [system identifier],
    // [public identifier], [declaration base URI], [notation name], and
    // [notation] are considered to be duplicate. An application may also
    // be able to detect that unparsed entities are duplicate through other
    // means. For instance, the URI resulting from combining the system
    // identifier and the declaration base URI is the same.
    public boolean isDuplicate(Object obj) {
      if (obj != null && obj instanceof UnparsedEntity) {
        UnparsedEntity other = (UnparsedEntity) obj;
        return Objects.equals(name, other.name)
            && Objects.equals(publicId, other.publicId)
            && Objects.equals(expandedSystemId, other.expandedSystemId)
            && Objects.equals(notation, other.notation);
      }
      return false;
    }
  }

  // The following methods are used for XML Base processing

  /**
   * Saves the current base URI to the top of the stack.
   */
  protected void saveBaseURI() {
    fBaseURIScope.push(fDepth);
    fBaseURI.push(fCurrentBaseURI.getBaseSystemId());
    fLiteralSystemID.push(fCurrentBaseURI.getLiteralSystemId());
    fExpandedSystemID.push(fCurrentBaseURI.getExpandedSystemId());
  }

  /**
   * Discards the URIs at the top of the stack, and restores the ones beneath it.
   */
  protected void restoreBaseURI() {
    fBaseURI.pop();
    fLiteralSystemID.pop();
    fExpandedSystemID.pop();
    fBaseURIScope.pop();
    fCurrentBaseURI.setBaseSystemId((String) fBaseURI.peek());
    fCurrentBaseURI.setLiteralSystemId((String) fLiteralSystemID.peek());
    fCurrentBaseURI.setExpandedSystemId((String) fExpandedSystemID.peek());
  }

  // The following methods are used for language processing

  /**
   * Saves the given language on the top of the stack.
   *
   * @param lanaguage the language to push onto the stack.
   */
  protected void saveLanguage(String language) {
    fLanguageScope.push(fDepth);
    fLanguageStack.push(language);
  }

  /**
   * Discards the language at the top of the stack, and returns the one beneath it.
   */
  public String restoreLanguage() {
    fLanguageStack.pop();
    fLanguageScope.pop();
    return (String) fLanguageStack.peek();
  }

  /**
   * Gets the base URI that was in use at that depth
   *
   * @return the base URI
   */
  public String getBaseURI(int depth) {
    int scope = scopeOfBaseURI(depth);
    return (String) fExpandedSystemID.elementAt(scope);
  }

  /**
   * Gets the language that was in use at that depth.
   *
   * @return the language
   */
  public String getLanguage(int depth) {
    int scope = scopeOfLanguage(depth);
    return (String) fLanguageStack.elementAt(scope);
  }

  /**
   * Returns a relative URI, which when resolved against the base URI at the
   * specified depth, will create the current base URI.
   * This is accomplished by merged the literal system IDs.
   *
   * @param depth the depth at which to start creating the relative URI
   * @return a relative URI to convert the base URI at the given depth to the current base URI
   */
  public String getRelativeURI(int depth) throws MalformedURIException {
    // The literal system id at the location given by "start" is *in focus* at
    // the given depth. So we need to adjust it to the next scope, so that we
    // only process out of focus literal system ids
    int start = scopeOfBaseURI(depth) + 1;
    if (start == fBaseURIScope.size()) {
      // If that is the last system id, then we don't need a relative URI
      return "";
    }
    URI uri = new URI("file", (String) fLiteralSystemID.elementAt(start));
    for (int i = start + 1; i < fBaseURIScope.size(); i++) {
      uri = new URI(uri, (String) fLiteralSystemID.elementAt(i));
    }
    return uri.getPath();
  }

  // We need to find two consecutive elements in the scope stack,
  // such that the first is lower than 'depth' (or equal), and the
  // second is higher.
  private int scopeOfBaseURI(int depth) {
    for (int i = fBaseURIScope.size() - 1; i >= 0; i--) {
      if (fBaseURIScope.elementAt(i) <= depth) {
        return i;
      }
    }
    // we should never get here, because 0 was put on the stack in startDocument()
    return -1;
  }

  private int scopeOfLanguage(int depth) {
    for (int i = fLanguageScope.size() - 1; i >= 0; i--) {
      if (fLanguageScope.elementAt(i) <= depth) {
        return i;
      }
    }
    // we should never get here, because 0 was put on the stack in startDocument()
    return -1;
  }

  /**
   * Search for a xml:base attribute, and if one is found, put the new base URI into
   * effect.
   */
  protected void processXMLBaseAttributes(XMLAttributes attributes) {
    String baseURIValue =
        attributes.getValue(NamespaceContext.XML_URI, "base");
    if (baseURIValue != null) {
      try {
        String expandedValue =
            XMLEntityManager.expandSystemId(
                baseURIValue,
                fCurrentBaseURI.getExpandedSystemId(),
                false);
        fCurrentBaseURI.setLiteralSystemId(baseURIValue);
        fCurrentBaseURI.setBaseSystemId(
            fCurrentBaseURI.getExpandedSystemId());
        fCurrentBaseURI.setExpandedSystemId(expandedValue);

        // push the new values on the stack
        saveBaseURI();
      } catch (MalformedURIException e) {
        // REVISIT: throw error here
      }
    }
  }

  /**
   * Search for a xml:lang attribute, and if one is found, put the new
   * [language] into effect.
   */
  protected void processXMLLangAttributes(XMLAttributes attributes) {
    String language = attributes.getValue(NamespaceContext.XML_URI, "lang");
    if (language != null) {
      fCurrentLanguage = language;
      saveLanguage(fCurrentLanguage);
    }
  }

  /**
   * Returns <code>true</code> if the given string
   * would be valid in an HTTP header.
   *
   * @param value string to check
   * @return <code>true</code> if the given string would be valid in an HTTP header
   */
  private boolean isValidInHTTPHeader(String value) {
    char ch;
    for (int i = value.length() - 1; i >= 0; --i) {
      ch = value.charAt(i);
      if (ch < 0x20 || ch > 0x7E) {
        return false;
      }
    }
    return true;
  }

  /**
   * Returns a new <code>XMLInputSource</code> from the given parameters.
   */
  private XMLInputSource createInputSource(String publicId,
      String systemId, String baseSystemId,
      String accept, String acceptLanguage) {

    HTTPInputSource httpSource = new HTTPInputSource(publicId, systemId, baseSystemId);
    if (accept != null && accept.length() > 0) {
      httpSource.setHTTPRequestProperty(XIncludeHandler.HTTP_ACCEPT, accept);
    }
    if (acceptLanguage != null && acceptLanguage.length() > 0) {
      httpSource.setHTTPRequestProperty(XIncludeHandler.HTTP_ACCEPT_LANGUAGE, acceptLanguage);
    }
    return httpSource;
  }

  // which ASCII characters need to be escaped
  private static final boolean gNeedEscaping[] = new boolean[128];
  // the first hex character if a character needs to be escaped
  private static final char gAfterEscaping1[] = new char[128];
  // the second hex character if a character needs to be escaped
  private static final char gAfterEscaping2[] = new char[128];
  private static final char[] gHexChs = {'0', '1', '2', '3', '4', '5', '6', '7',
      '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

  // initialize the above 3 arrays
  static {
    char[] escChs = {' ', '<', '>', '"', '{', '}', '|', '\\', '^', '`'};
    int len = escChs.length;
    char ch;
    for (int i = 0; i < len; i++) {
      ch = escChs[i];
      gNeedEscaping[ch] = true;
      gAfterEscaping1[ch] = gHexChs[ch >> 4];
      gAfterEscaping2[ch] = gHexChs[ch & 0xf];
    }
  }

  //
  // Escape an href value according to (4.1.1):
  //
  // To convert the value of the href attribute to an IRI reference, the following characters must be escaped:
  // space #x20
  // the delimiters < #x3C, > #x3E and " #x22
  // the unwise characters { #x7B, } #x7D, | #x7C, \ #x5C, ^ #x5E and ` #x60
  //
  // To convert an IRI reference to a URI reference, the following characters must also be escaped:
  // the Unicode plane 0 characters #xA0 - #xD7FF, #xF900-#xFDCF, #xFDF0-#xFFEF
  // the Unicode plane 1-14 characters #x10000-#x1FFFD ... #xE0000-#xEFFFD
  //
  private String escapeHref(String href) {
    int len = href.length();
    int ch;
    final StringBuilder buffer = new StringBuilder(len * 3);

    // for each character in the href
    int i = 0;
    for (; i < len; i++) {
      ch = href.charAt(i);
      // if it's not an ASCII character (excluding 0x7F), break here, and use UTF-8 encoding
      if (ch > 0x7E) {
        break;
      }
      // abort: href does not allow this character
      if (ch < 0x20) {
        return href;
      }
      if (gNeedEscaping[ch]) {
        buffer.append('%');
        buffer.append(gAfterEscaping1[ch]);
        buffer.append(gAfterEscaping2[ch]);
      } else {
        buffer.append((char) ch);
      }
    }

    // we saw some non-ascii character
    if (i < len) {
      // check if remainder of href contains any illegal characters before proceeding
      for (int j = i; j < len; ++j) {
        ch = href.charAt(j);
        if ((ch >= 0x20 && ch <= 0x7E) ||
            (ch >= 0xA0 && ch <= 0xD7FF) ||
            (ch >= 0xF900 && ch <= 0xFDCF) ||
            (ch >= 0xFDF0 && ch <= 0xFFEF)) {
          continue;
        }
        if (XMLChar.isHighSurrogate(ch) && ++j < len) {
          int ch2 = href.charAt(j);
          if (XMLChar.isLowSurrogate(ch2)) {
            ch2 = XMLChar.supplemental((char) ch, (char) ch2);
            if (ch2 < 0xF0000 && (ch2 & 0xFFFF) <= 0xFFFD) {
              continue;
            }
          }
        }
        // abort: href does not allow this character
        return href;
      }

      // get UTF-8 bytes for the remaining sub-string
      byte[] bytes = null;
      byte b;
      try {
        bytes = href.substring(i).getBytes("UTF-8");
      } catch (java.io.UnsupportedEncodingException e) {
        // should never happen
        return href;
      }
      len = bytes.length;

      // for each byte
      for (i = 0; i < len; i++) {
        b = bytes[i];
        // for non-ascii character: make it positive, then escape
        if (b < 0) {
          ch = b + 256;
          buffer.append('%');
          buffer.append(gHexChs[ch >> 4]);
          buffer.append(gHexChs[ch & 0xf]);
        } else if (gNeedEscaping[b]) {
          buffer.append('%');
          buffer.append(gAfterEscaping1[b]);
          buffer.append(gAfterEscaping2[b]);
        } else {
          buffer.append((char) b);
        }
      }
    }

    // If escaping happened, create a new string;
    // otherwise, return the orginal one.
    if (buffer.length() != len) {
      return buffer.toString();
    } else {
      return href;
    }
  }
}
